﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Logger;
using SocketCore.Event;

namespace SocketCore.Network
{
    /// <summary>
    /// store information about each IO Session with client (for each client socket)
    /// main duties: receiving data and handling context-based parameters
    /// Holds the appropriate SocketSerivce and Socket
    /// </summary>
    public class IoSession : IDisposable
    {
        #region private members

        private IDictionary<string, object> attributes;
        private ReaderWriterLock attributesLock = new ReaderWriterLock();
        private IDictionary<string, bool> blackList;
        private const int BODY = 1;
        private readonly object busyLock = new object();
        private bool disposed = false;
        private const int HEADER = 0;
        private bool isClosed = false;
        private bool isKick;
        private EndPoint localHost;
        private IDictionary<string, object> memAtributes;
        private ReaderWriterLock memAttributesLock = new ReaderWriterLock();
        private MemoryStream messageBufferStream;
        private EndPoint remoteHost;
        private Socket socket;
        private ISocketService socketService;
        private const int WAIT_TIME = 30000;

        private Timer timerHeartbeat;
        private const int TIMER_HEARTBEAT_ELLAPSE = 3000;
        private const int TIMER_HEARTBEAT_TIMEOUT = 10000;

        #endregion

        public IoSession(Socket socket, ISocketService service)
        {
            this.socketService = service;
            this.socket = socket;
            if (socket != null)
            {
                this.localHost = socket.LocalEndPoint;
                this.remoteHost = socket.RemoteEndPoint;
            }
            memAtributes = new Dictionary<string, object>();
            attributes = new Dictionary<string, object>(10);
            blackList = new Dictionary<string, bool>();
            timerHeartbeat = new Timer(TimerHeartbeatCallback, this, TIMER_HEARTBEAT_ELLAPSE, TIMER_HEARTBEAT_ELLAPSE);
        }

        public void TimerHeartbeatCallback(object stateObj)
        {
            IoSession session = (IoSession) stateObj;
            Console.WriteLine("TimerHeartbeat!");
            if(session.IsConnected)
            {
                Message msg = new Message(PackageTypeEnum.Heartbeat, true, "ping");
                session.socketService.Send(session, msg);

                // Ticks is measured in 100 nano-seconds (vcl).
                DateTime dt = DateTime.Now;
                long span = (dt.Ticks - session.GetMemProperty<long>("LastAccess"))/10000;

                if (span > TIMER_HEARTBEAT_TIMEOUT)
                {
                    //Console.WriteLine(string.Format("TimerHeartbeat: Gonna close, span = {0}.", span));
                    //Console.WriteLine(string.Format("now = {0}, {1}, las = {2}", dt, dt.Ticks, session.GetMemProperty<long>("LastAccess")));
                    session.Close();
                }
            }
            else
            {
                if (timerHeartbeat != null)
                {
                    timerHeartbeat.Dispose();
                    timerHeartbeat = null;
                }
                
            }
        }

        public void AddOrBlackList(string username)
        {
            if (this.blackList.ContainsKey(username))
            {
                this.blackList.Remove(username);
            }
            else
            {
                this.blackList.Add(username, true);
            }
        }

        public void AddProperty(string key, object value)
        {
            this.attributesLock.AcquireWriterLock(WAIT_TIME);
            try
            {
                if (this.attributes.ContainsKey(key))
                {
                    this.attributes.Remove(key);
                }
                this.attributes.Add(key, value);
            }
            finally
            {
                this.attributesLock.ReleaseWriterLock();
            }
        }

        public void AddProperty(string key, object value, bool isMem)
        {
            if (!isMem)
            {
                this.AddProperty(key, value);
            }
            else
            {
                this.memAttributesLock.AcquireWriterLock(WAIT_TIME);
                try
                {
                    if (this.memAtributes.ContainsKey(key))
                    {
                        this.memAtributes.Remove(key);
                    }
                    this.memAtributes.Add(key, value);
                }
                finally
                {
                    this.memAttributesLock.ReleaseWriterLock();
                }
            }
        }

        public void Clear()
        {
            try
            {
                if (this.messageBufferStream != null)
                {
                    try
                    {
                        this.messageBufferStream.Close();
                    }
                    catch
                    {
                    }
                }
                this.attributesLock.AcquireWriterLock(WAIT_TIME);
                try
                {
                    this.attributes.Clear();
                }
                finally
                {
                    this.attributesLock.ReleaseWriterLock();
                }
                this.memAttributesLock.AcquireWriterLock(WAIT_TIME);
                try
                {
                    this.memAtributes.Clear();
                }
                finally
                {
                    this.memAttributesLock.ReleaseWriterLock();
                }
            }
            catch
            {
            }
        }

        public void Close()
        {
            timerHeartbeat.Dispose();
            timerHeartbeat = null;
            socketService.DisConnect(this);
            isClosed = true;
        }

        public void Dispose()
        {
            if (!this.disposed)
            {
                this.Dispose(true);
                GC.SuppressFinalize(this);
                this.disposed = true;
            }
        }

        protected virtual void Dispose(bool disposing)
        {
            if (this.attributesLock != null)
            {
                this.attributesLock = null;
            }
            if (this.memAttributesLock != null)
            {
                this.memAttributesLock = null;
            }
            if (this.messageBufferStream != null)
            {
                try
                {
                    this.messageBufferStream.Dispose();
                    this.messageBufferStream = null;
                }
                catch
                {
                }
            }
            this.attributes.Clear();
            this.memAtributes.Clear();
        }

        ~IoSession()
        {
            try
            {
                this.Dispose(false);
            }
            catch (System.Exception e)
            {
                Log.WriteLog("Error when destroy IoSession: " + e);
            }
        }

        public T GetMemProperty<T>(string key)
        {
            T ret;
            this.memAttributesLock.AcquireReaderLock(WAIT_TIME);
            try
            {
                if (!this.memAtributes.ContainsKey(key))
                {
                    this.memAtributes.Add(key, null);
                }
                T res = (T) this.memAtributes[key];
                ret = res;
            }
            finally
            {
                this.memAttributesLock.ReleaseReaderLock();
            }
            return ret;
        }

        public T GetProperty<T>(string key)
        {
            T ret;
            this.attributesLock.AcquireReaderLock(WAIT_TIME);
            try
            {
                if (!this.attributes.ContainsKey(key))
                {
                    return default(T);
                }
                T res = (T)attributes[key];
                ret = res;
            }
            finally
            {
                this.attributesLock.ReleaseReaderLock();
            }
            return ret;
        }

        private void ProcessBody(IAsyncResult ar)
        {
            try
            {
                if (this.IsConnected)
                {
                    byte[] readBytes = (byte[]) ar.AsyncState;
                    int byteRead = this.socket.EndReceive(ar);
                    if (byteRead != 0)
                    {
                        messageBufferStream.Write(readBytes, 0, byteRead);
                    }
                    else
                    {
                        Close();
                        return;
                    }
                    long remaining = readBytes.Length - byteRead;
                    if (remaining > 0L)
                    {
                        ReadBuffer(remaining, 1);
                    }
                    else
                    {
                        this.messageBufferStream.Position = 0L;
                        try
                        {
                            Message message = socketService.Decoder.Decode(messageBufferStream);
                            if (message == null)
                            {
                                Close();
                                return;
                            }
                            if (socketService.CheckDos(this))
                            {
                                Log.WriteLog("Dos detected from: " + RemoteHost);
                                Close();
                                return;
                            }

                            ThreadPool.QueueUserWorkItem(new WaitCallback(socketService.OnMessageReceived), new MessageEvent(this, message));
                        }
                        catch (System.Exception e)
                        {
                            Console.WriteLine("Error while processing mail: " + e);
                            Log.WriteLog("Error while processing mail: " + e);
                        }
                        ReadMessage();
                    }
                }
                else
                {
                    Close();
                }
            }
            catch (System.Exception e)
            {
                Log.WriteLog(string.Concat(new object[] { "Error while reading message from: ", remoteHost, ": ", e }));
                this.socketService.NotifyError(this, e);
            }
        }

        private void ProcessHeader(IAsyncResult ar)
        {
            try
            {
                if (this.IsConnected)
                {
                    int byteRead = this.socket.EndReceive(ar);
                    byte[] readBytes = (byte[]) ar.AsyncState;
                    if (byteRead != 0)
                    {
                        messageBufferStream.Write(readBytes, 0, byteRead);
                    }
                    else
                    {
                        Close();
                        return;
                    }
                    long remaining = 4L - messageBufferStream.Length;
                    if (remaining > 0L)
                    {
                        ReadBuffer(remaining, 0);
                    }
                    else
                    {
                        messageBufferStream.Position = 0L;
                        int mailSize = ReadInt(this.messageBufferStream);
                        messageBufferStream.Close();
                        messageBufferStream = new MemoryStream();
                        ReadBuffer((long) mailSize, 1);
                    }
                }
                else
                {
                    Close();
                }
            }
            catch (System.Exception e)
            {
                Log.WriteLog(string.Concat(new object[] { "Error while reading message from: ", this.remoteHost, ": ", e }));
                this.socketService.NotifyError(this, e);
            }
        }

        private void ReadBuffer(long remainLen, int type)
        {
            try
            {
                if (this.IsConnected)
                {
                    byte[] buffer = new byte[remainLen];
                    AsyncCallback callBack = null;
                    switch (type)
                    {
                        case 0:
                            callBack = new AsyncCallback(this.ProcessHeader);
                            break;

                        case 1:
                            callBack = new AsyncCallback(this.ProcessBody);
                            break;

                        default:
                            throw new ArgumentException("Cannot recognize type: " + type);
                    }
                    this.socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, callBack, buffer);
                }
                else
                {
                    Close();
                }
            }
            catch (System.Exception e)
            {
                Log.WriteLog(string.Concat(new object[] { "Error while reading message from: ", this.remoteHost, ": ", e }));
                this.socketService.NotifyError(this, e);
            }
        }

        private static int ReadInt(Stream stream)
        {
            int i = 0;
            for (int j = 0; j < 4; j++)
            {
                i = i << 8;
                int readByte = stream.ReadByte();
                i |= readByte;
            }
            return i;
        }

        public void ReadMessage()
        {
            try
            {
                if (this.IsConnected)
                {
                    if (this.messageBufferStream != null)
                    {
                        try
                        {
                            this.messageBufferStream.Close();
                        }
                        catch
                        {
                        }
                    }
                    messageBufferStream = new MemoryStream();
                    ReadBuffer(4L, 0);
                }
                else
                {
                    this.Close();
                }
            }
            catch (System.Exception e)
            {
                Log.WriteLog(string.Concat(new object[] { "Error while reading message from: ", this.remoteHost, ": ", e }));
                this.socketService.NotifyError(this, e);
            }
        }

        public void RemoveProperty<T>(string key)
        {
            this.attributesLock.AcquireWriterLock(0x7530);
            try
            {
                this.attributes.Remove(key);
            }
            finally
            {
                this.attributesLock.ReleaseWriterLock();
            }
        }

        public void SetBlackList(IDictionary<string, bool> blacklist)
        {
            this.blackList = blacklist;
        }

        public void Write(byte[] serializeMessage)
        {
            this.socketService.Send(this, serializeMessage);
        }

        //public void WriteMessage(Message message)
        //{
        //    if ((message.Src == null) || !this.blackList.ContainsKey(message.Src))
        //    {
        //        this.socketService.Send(this, message);
        //    }
        //}

        #region Properties

        public IDictionary<string, object> Attributes
        {
            get
            {
                return this.attributes;
            }
        }

        public object BusyLock
        {
            get
            {
                return this.busyLock;
            }
        }

        public bool IsConnected
        {
            get
            {
                return (((!this.isClosed && !this.isKick) && (this.socket != null)) && this.socket.Connected);
            }
        }

        public bool IsKick
        {
            get
            {
                return this.isKick;
            }
            set
            {
                this.isKick = value;
            }
        }

        public EndPoint LocalHost
        {
            get
            {
                return this.localHost;
            }
            set
            {
                this.localHost = value;
            }
        }

        public EndPoint RemoteHost
        {
            get
            {
                return this.remoteHost;
            }
            set
            {
                this.remoteHost = value;
            }
        }

        public Socket Socket
        {
            get
            {
                return this.socket;
            }
            set
            {
                this.socket = value;
            }
        }

        #endregion
    }
}
